package weeny.mds;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import jp.sourceforge.talisman.mds.Item;
import jp.sourceforge.talisman.mds.MdsMethod;
import jp.sourceforge.talisman.mds.Table;

/**
 * Utility to do the multi-dimensional scaling. Basically, you should:
 *   1) implement {@link #toVector(Object)} function,
 *   2) then call {@link #add(Object)} to add entities, 
 *   3) and finally call {@link #calc(int)} to get the rescaled result.
 * 
 * The vector distance is decided by {@link #distance(double[], double[])}. By
 * default, it is using cosine distance, but you can override if necessary.
 * 
 * @author weeny
 */
public abstract class AbstractMdsUtil {
	
	/**
	 * Create a new vector for the object. The vector will be then passed into
	 * MDS component for the scaling. So you should override this function for
	 * different types of data.
	 */
	protected abstract double[] toVector(Object o);
	
	/**
	 * Given two vector, calculate the distance between them. It is important
	 * for MDS, since MDS needs to know the distance between any two entities.
	 * By default, we use cosine distance. But you can override if necessary.
	 * @param vec1
	 * @param vec2
	 * @return
	 */
	protected double distance(double[] vec1, double[] vec2){
		return cosinDist(vec1, vec2);
	}
	
	/**
	 * A dictance function measuring the cosin distance between two vectors. If
	 * the input vectors are null or do not have the same length, an exception
	 * will be thrown.
	 */
	public static double cosinDist(double[] vec1, double[] vec2){
		if (vec1 == null || vec2 == null || vec1.length != vec2.length) {
			throw new IllegalArgumentException("wrong input");
		}
		double dotProduct = 0;
		double vec1Sq = 0;
		double vec2Sq = 0;
		for(int i = 0; i<vec1.length; i++){
			dotProduct += vec1[i] * vec2[i];
			vec1Sq += vec1[i] * vec1[i];
			vec2Sq += vec2[i] * vec2[i];
		}
		return dotProduct / Math.sqrt(vec1Sq*vec2Sq);
	}
	
	private Table table = new Table();
	
	/**
	 * Add a new object into this utility. It does not filter the identical
	 * objects.
	 */
	public void add(Object o){
		Set keySet = table.getKeySet();
		for (Object object : keySet) {
			table.addValue(o, object, distance(toVector(o), toVector(object)));
		}
		table.addValue(o, o, 0);
	}
	/**
	 * It is just a convenience function, who calls {@link #add(Object)} for
	 * everyone in this collection
	 */
	public void add(Collection coll){
		add(coll.iterator());
	}
	/**
	 * It is just a convenience function, who calls {@link #add(Object)} for
	 * everyone in the iterator
	 */
	public void add(Iterator it){
		while (it.hasNext()){
			add((Object) it.next());
		}
	}
	/**
	 * Calculate the result of the MDS, you need to call {@link #add(Object)}
	 * first before you call this function.
	 * @param dimCnt the dimension of the target space.
	 * @return a map mapping object to double[] (point in hyper-space). The
	 * space dimension is decided by the passed-in dimCnt. the key objects are
	 * the ones inserted by calling {@link #add(Object)} function. the value
	 * arrays are decided by function {@link #toVector(Object)}.
	 */
	public Map calc(int dimCnt){
		MdsMethod<Object> method = new MdsMethod<Object>(table);
		Set keys = table.getKeySet();
		Map<String, Object> name2Obj = new TreeMap<String, Object>();
		for(Object key : keys){
			if (name2Obj.containsKey(key.toString())) {
				throw new IllegalStateException("duplicated name");
			}
			name2Obj.put(key.toString(), key);
		}
		method.setLimit(dimCnt);
		Map<Object, double[]> result = new HashMap<Object, double[]>();
		for(Item item : method){
			double[] scale = new double[dimCnt];
			for(int i = 0; i<dimCnt; i++){
				double doubleValue = item.get(i).doubleValue();
				if (Double.isNaN(doubleValue)) {
					//happens in calculate(), they use Math.sqrt(eiganvalue),
					//where eiganvalue <0
					throw new IllegalStateException("have a NaN coordinates " +
							"(the inter-point distance should be Euclidean!!)");
				}
				scale[i] = item.get(i).doubleValue();
			}
			Object obj = name2Obj.get(item.getName());
			if (obj == null) {
				throw new IllegalStateException("should be a key");
			}
			result.put(obj, scale);
		}
		return result;
	}
}
